package com.uniandes.edu.co.procesar.anotacion;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import com.uniandes.edu.co.anotaciones.LogD;
import com.uniandes.edu.co.anotaciones.LogInt;
import com.uniandes.edu.co.anotaciones.LogT;
import com.uniandes.edu.co.anotaciones.LogsD;
import com.uniandes.edu.co.anotaciones.LogsInt;
import com.uniandes.edu.co.anotaciones.LogsT;
import com.uniandes.edu.co.anotaciones.Plan;
import com.uniandes.edu.co.dto.Ciclo;
import com.uniandes.edu.co.dto.Interrupcion;
import com.uniandes.edu.co.dto.InterrupcionReal;
import com.uniandes.edu.co.dto.PlanQ;
import com.uniandes.edu.co.dto.Tarea;
import com.uniandes.edu.co.dto.Tsp;
import com.uniandes.edu.co.excepciones.FormatoFechaInvalido;
import com.uniandes.edu.co.excepciones.LecturaArcivoException;
import com.uniandes.edu.co.excepciones.ProcesamientoArchivoException;

/**
 * clase encargada de procesar las anotaciones
 * @author Juan Camilo Sanchez
 *
 */
public class ProcesarAnotaciones {

	/**
	 * Extensión para los archivos buscados
	 */
	private static final String EXTENSION_CLASS = ".class";

	/**
	 * Ruta para el URL de los archivos cargados dinámicamente
	 */
	private static final String URL_FILE = "file://";

	/**
	 * Método privado de la clase URIClassLoader, utilizado para
	 * cambiar por reflection la visibilidad del método y poder adicionar
	 * elementos al classpath dinámicamente
	 */
	private static final String ADD_URL_METHOD = "addURL";
	
	/**
	 * Verifica si ya fue agregado el classpath correcto
	 */
	private boolean agregadoClasspath=false;
		
	/**
	 * instancia unica de la clase
	 */
	@LogsT({@LogT(taskId=99, idPersona=20112, idCiclo=2, date="20/03/2011", min=1, porcentajeRealValorGanado=1)})
	static private ProcesarAnotaciones solitario = null;

	/**
	 * metodo que valida que solo este creado una instancia de
	 * la clase
	 * @return la instancia de la clase
	 */
	@LogsT({@LogT(taskId=99, idPersona=20112, idCiclo=2, date="20/03/2011", min=1, porcentajeRealValorGanado=1)})
	static public ProcesarAnotaciones getSingleton() {
		if (solitario == null) {
			solitario = new ProcesarAnotaciones();
		}
		return solitario;
	}

	/**
	 * Procesa la clase pasada por parámetros
	 * @param clase Clase que se desea procesar
	 * @throws ProcesamientoArchivoException 
	 */
	@SuppressWarnings("rawtypes")
	@Plan(idCiclo=2, size=3555, time=234)
	@LogsInt(
		{
			@LogInt(date="20/03/2011", idCiclo=2, idPersona=20113, min=3, intId=2),// Chat
			@LogInt(date="20/03/2011", idCiclo=2, idPersona=20113, min=10, intId=2)// Chat
		}
	)
	public void procesarClase(Class clase) throws ProcesamientoArchivoException {
		ArrayList<Annotation> listaAnotaciones=obtenerAnotaciones(clase);
		for (Annotation anotacion : listaAnotaciones) {
			/*
			 * Ya no se va a utilizar
			if(anotacion instanceof Plan)
			{
				Plan plan=(Plan)anotacion;
				Ciclo ciclo=obtenerCiclo(plan.idCiclo());
				if(ciclo==null)
				{
					throw new ProcesamientoArchivoException("No existe el ciclo "+plan.idCiclo()+" en la anotación "+anotacion+"\nen la clase "+clase.getSimpleName());
				}
			}
			 */
			if(anotacion instanceof LogsT)
			{
				LogsT logsT=(LogsT)anotacion;
				for (LogT logT : logsT.value()) {
					Tarea tarea=obtenerTarea(logT.taskId(), logT.idCiclo());
					if(tarea==null)
					{
						throw new ProcesamientoArchivoException("La tarea de la anotacion\n"+logT.toString()+"\npara la clase "+clase.getSimpleName()+" no existe");
					}
					tarea.setTiempoReal(logT.min());
					tarea.setValorGanadoReal(logT.porcentajeRealValorGanado());
				}
			}
			if(anotacion instanceof LogsD)
			{
				LogsD logsD=(LogsD)anotacion;
				for (LogD logD : logsD.value()) {
					Ciclo ciclo=obtenerCiclo(logD.idCiclo());
					if(ciclo==null)
					{
						throw new ProcesamientoArchivoException("El ciclo para la anotación "+logD.toString()+" no existe");
					}
					ciclo.adicionarDefectoRealPlanQPorEtapa(logD.etapa());
				}
			}
			if(anotacion instanceof LogsInt)
			{
				LogsInt logsInt=(LogsInt)anotacion;
				for (LogInt logInt : logsInt.value()) {
					Ciclo ciclo=obtenerCiclo(logInt.idCiclo());
					Interrupcion interrupcion=obtenerInterrupcion(ciclo.getInterrupciones(), logInt.intId());
					if(interrupcion==null)
					{
						throw new ProcesamientoArchivoException("La interrupcion para la anotacion\n"+logInt.toString()+"\npara la clase "+clase.getSimpleName()+" no existe");
					}
					InterrupcionReal interrupcionReal;
					try {
						interrupcionReal = new InterrupcionReal(interrupcion, logInt.min(), logInt.date());
					} catch (FormatoFechaInvalido e) {
						System.err.println("Error en el formato de la fecha para la anotación "+logInt.toString()+", no será tenida en cuenta ["+e.getMessage()+"]");
						/*
						 * Como las fechas no son importantes para el reporte, se puede ovbiar el error
						 * de la conversión de fecha, en este caso queda null
						 */
						interrupcionReal = new InterrupcionReal(interrupcion, logInt.min());
					}
					if(ciclo.getInterrupcionesReales()==null)
					{
						ciclo.setInterrupcionesReales(new LinkedList<InterrupcionReal>());
					}
					ciclo.getInterrupcionesReales().add(interrupcionReal);
				}
			}
		}
	}

	/**
	 * Obtiene la interrupción solicitada desde el ID
	 * @param interrupciones Listado de las interrupciones
	 * @param idInterrupcion Id de la interrupción solicitada
	 * @return {@link Interrupcion} Interrupción encontrada, null si no existe la interrupción con el id pasado por parámetros
	 */
	private Interrupcion obtenerInterrupcion(List<Interrupcion> interrupciones, int idInterrupcion) {
		for (Interrupcion interrupcion : interrupciones) {
			if((idInterrupcion+"").equals(interrupcion.getIdInterrupcion()))
			{
				return interrupcion;
			}
		}
		return null;
	}

	/**
	 * Obtiene las anotaciones de la clase y de los métodos
	 * @param clase Clase a procesar
	 * @return Lista de todas las anotaciones existentes
	 */
	@SuppressWarnings("rawtypes")
	public ArrayList<Annotation> obtenerAnotaciones(Class clase) {
		ArrayList<Annotation> listaAnotaciones=new ArrayList<Annotation>();
		Annotation[] anotaciones=clase.getDeclaredAnnotations();
		adicionarAnotacionesALista(listaAnotaciones, anotaciones);
		Method[] metodos=clase.getDeclaredMethods();
		for (Method metodo : metodos) {
			anotaciones=metodo.getDeclaredAnnotations();
			adicionarAnotacionesALista(listaAnotaciones, anotaciones);
		}
		return listaAnotaciones;
	}

	/**
	 * Adiciona las anotaciones a la lista de todas las anotaciones de la clase
	 * @param listaAnotaciones Lista destino
	 * @param anotaciones Array con la lista de anotaciones a agregar.
	 */
	private void adicionarAnotacionesALista(ArrayList<Annotation> listaAnotaciones, Annotation[] anotaciones) {
		for (Annotation annotation : anotaciones) {
			listaAnotaciones.add(annotation);
		}
	}

	/**
	 * Obtener la tarea dado el ID
	 * @param taskId Id de la tarea buscada
	 * @param idCiclo id del ciclo de la tarea
	 * @return Tarea buscada
	 */
	public Tarea obtenerTarea(double taskId, int idCiclo) {
		Ciclo ciclo=obtenerCiclo(idCiclo);
		if(ciclo!=null)
		{
			for (Tarea tarea : ciclo.getTareas()) {
				if(Double.parseDouble(tarea.getIdTarea())==taskId)
				{
					return tarea;
				}
			}
		}
		return null;
	}

	/**
	 * Obtener el ciclo dado un ID
	 * @param idCiclo
	 * @return Ciclo buscado, null de lo contrario
	 */
	public Ciclo obtenerCiclo(int idCiclo) {
		List<Ciclo> ciclos=Tsp.getSingleton().getCiclos();
		for (Ciclo ciclo : ciclos) {
			if(ciclo.getIdCiclo()==idCiclo)
			{
				return ciclo;
			}
		}
		return null;
	}

	/**
	 * Procesa un directorio
	 * @param directorio
	 * @throws LecturaArcivoException
	 * @throws ProcesamientoArchivoException 
	 */
	public void procesarDirectorio(File directorio) throws LecturaArcivoException, ProcesamientoArchivoException {
		URLClassLoader classLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
		procesarClasesRecursivo(directorio, classLoader);
	}

	/**
	 * Cargar las clases recursivamente para el procesamiento de las anotaciones
	 * @param directorio directorio que se desea procesar
	 * @param classLoader Cargador de clases dinámico
	 * @throws LecturaArcivoException 
	 * @throws ProcesamientoArchivoException 
	 */
	@SuppressWarnings("rawtypes")
	private void procesarClasesRecursivo(File directorio, URLClassLoader classLoader) throws LecturaArcivoException, ProcesamientoArchivoException {
		if(directorio.exists())
		{
			/*
			 * Si es un directorio se buscan los subdirectorios y los archivos
			 */
			if(directorio.isDirectory())
			{
				/*
				 * Se listan los archivos
				 */
				File[] archivos=directorio.listFiles();
				
				for(File archivo:archivos)
				{
					/*
					 * Si es un directorio entonces se llama al método recursivo
					 * de lo contrario se verifica si es un .class y se procesa
					 */
					if(archivo.isDirectory())
					{
						procesarClasesRecursivo(archivo, classLoader);
					}
					else if(archivo.getName().endsWith(EXTENSION_CLASS))
					{
						Class clase=cargarClase(archivo, classLoader);
						procesarClase(clase);
					}
				}
			}
			else
			{
				throw new LecturaArcivoException("No se seleccionó un directorio");
			}
		}
		else
		{
			throw new LecturaArcivoException("No existe el directorio "+directorio.getAbsolutePath());
		}
	}

	/**
	 * Buscar las anotaciones de una clase
	 * @param archivo Archivo que se desea procesar
	 * @param classLoader Cargador dinámico de clases
	 * @return Clase cargada dinámicamente
	 * @throws LecturaArcivoException
	 */
	@LogsInt(
		{
			@LogInt(date="20/03/2011", idCiclo=2, idPersona=20113, min=3, intId=1)// Televisor
		}
	)
	@SuppressWarnings("rawtypes")
	public Class cargarClase(File archivo, URLClassLoader classLoader)
			throws LecturaArcivoException {

		File padre=archivo;
		String path=archivo.getParentFile().getAbsolutePath();
		Class clase=null;
		String nombreClase=archivo.getName().split("\\.")[0];
		while(padre!=null && clase==null)
		{
			try {
				if(!agregadoClasspath)
				{
					/*
					 * Esta parte se hace para no agregar directorios al classpath
					 * que no son válidos
					 */
					path = verificarPath(path);
					URL url = new URL(URL_FILE+path);
					URLClassLoader classLoaderTempo=URLClassLoader.newInstance(new URL[]{url});
					classLoaderTempo.loadClass(nombreClase);
					/*
					 * Si pasó hasta aquí es por que el path es válido para agregar
					 */
					adicionarURL(path, classLoader);
					clase=Class.forName(nombreClase);
					agregadoClasspath=true;
				}
				else
				{
					clase=Class.forName(nombreClase);
				}
			} catch (NoClassDefFoundError e) {
				padre=padre.getParentFile();
				if(padre!=null)
				{
					nombreClase=padre.getName()+"."+nombreClase;
					path=padre.getParentFile().getAbsolutePath();
				}
			} catch (ClassNotFoundException e) {
				padre=padre.getParentFile();
				if(padre!=null)
				{
					nombreClase=padre.getName()+"."+nombreClase;
					path=padre.getParentFile().getAbsolutePath();
				}
			} catch (MalformedURLException e) {
				throw new LecturaArcivoException("Error creando el path para el URL "+URL_FILE+archivo.getAbsolutePath());
			}
		}
		if(clase==null)
		{
			// No se pudo cargar la clase dinámicamente
			throw new LecturaArcivoException("La clase no puede ser instanciada dinámicamente");
		}
		
		return clase;
	}

	/**
	 * Verifica si el path si tiene correcta la sintaxis
	 * @param path Path que se desea evaluar
	 * @return path corregido
	 */
	private String verificarPath(String path) {
		if(!path.endsWith(File.separator))
		{
			path+=File.separator;
		}
		return path;
	}

	/**
	 * Adiciona un url al classpath
	 * @param path Path que se desea agregar al classpath
	 * @param URLClassLoader para cargar las clases
	 * @throws LecturaArcivoException
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void adicionarURL(String path, URLClassLoader classLoader) throws LecturaArcivoException
	{
		try {
			URL url = new URL(URL_FILE+path);
			Class sysclass = URLClassLoader.class;
			Class[] parametros = new Class[] { URL.class };
			Method method = sysclass.getDeclaredMethod(ADD_URL_METHOD, parametros);
			method.setAccessible(true);
			method.invoke(classLoader, new Object[] { url });
		} catch (MalformedURLException e) {
			throw new LecturaArcivoException("El directorio es inacesible");
		} catch (SecurityException e) {
			throw new LecturaArcivoException("Verifique las opciones de seguridad de java");
		} catch (NoSuchMethodException e) {
			throw new LecturaArcivoException("El método addURL no existe, por favor verifique la versión de java 5 o superior");
		} catch (IllegalArgumentException e) {
			throw new LecturaArcivoException("El método addURL no se llamó correctamente");
		} catch (IllegalAccessException e) {
			throw new LecturaArcivoException("El método addURL no es accesible");
		} catch (InvocationTargetException e) {
			throw new LecturaArcivoException("El método addURL no se llamó correctamente");
		}
	}

	public void limpiarInformacion() {
		// Limpiar info ciclo calidad
		List<Ciclo> ciclos=Tsp.getSingleton().getCiclos();
		for (Ciclo ciclo : ciclos) {
			ciclo.setDefectosRealesRemovidos(new PlanQ());
		}
	}
}